import com.google.gson2.Gson;
import ferp.center.network.request.RequestConfigBidNNUpdate;
import ferp.center.network.response.ResponseBidDataGet;
import ferp.core.ai.nn.Adaptation;
import ferp.core.ai.nn.NN;
import ferp.core.ai.nn.activation.Sigmoid;
import ferp.core.game.Game;
import ferp.core.util.Convert;
import training.nn.Backprop;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.LinkedList;
import java.util.List;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;

/**
 * User: igorgok
 * Date: 16/07/2014 16:38
 */
public class Trainer
{
  public static void main(String[] args) throws IOException, ClassNotFoundException
  {
    //download();
    //train();
    upload("bid.217.nn");
    //upload("bid.217.nn");
    //load("bid.217.nn");
  }

  private static void train() throws IOException, ClassNotFoundException
  {
    // prepare the training set
    System.out.println("prepairing training set...");

    long start = System.currentTimeMillis();
    List<ResponseBidDataGet.Record> bids = getAllBids();
    double[][] set = new double[bids.size()][];
    int row = 0;

    for (ResponseBidDataGet.Record b : bids)
      set[row++] = Adaptation.bid2nnts(b.cards, b.hand, b.starting, b.last, b.penalty, b.result);

    long prepared = System.currentTimeMillis();

    // train
    int os = Adaptation.BID_NN_NUM_OF_OUTPUTS, is = set[0].length - os;
    NN nn = new NN(new int[]{is, 10, os}, Sigmoid.instance);
    String filename = getNNFileName("bid", nn);

    // train
    System.out.println("training the NN...");

    long epochs = new Backprop(nn).train(set, 0.2, 0.9, 0.05);
    long trained = System.currentTimeMillis();

    // test
    int e1 = test(nn, set, "original");
    long tested = System.currentTimeMillis();
    // serialize and save to file
    long size = serialize(nn, filename);
    long serialized = System.currentTimeMillis();

    // loading serialized NN and testing again
    NN nn2 = load(filename);
    long loaded = System.currentTimeMillis();
    int e2 = test(nn2, set, "reloaded");

    System.out.println("training set size: " + set.length);
    System.out.println("final error rate: " + e1 + "/" + e2 + "%");
    System.out.println("times: prepare=" + (prepared - start) + "ms, train=" + (trained - prepared) / 1000 +
        "s, test=" + (tested - trained) + "ms, serialize=" + (serialized - tested) + "ms, load=" + (loaded - serialized));
    System.out.println("serialized NN size: " + size);
  }
  
  private static List<ResponseBidDataGet.Record> getAllBids() throws IOException
  {
    BufferedReader br = new BufferedReader(new FileReader(FILE));
    List<ResponseBidDataGet.Record> bids = new LinkedList<ResponseBidDataGet.Record>();

    for (String s = br.readLine(); s != null; s = br.readLine())
    {
      ResponseBidDataGet rbdg = Game.gson.fromJson(s, ResponseBidDataGet.class);

      bids.addAll(rbdg.records);
    }
    
    br.close();
    
    return bids;
  }

  // test the net and return error in %
  private static int test(NN nn, double[][] set, String what)
  {
    int success = 0;

    System.out.println("testing " + what + " NN...");

    for (double[] r : set)
    {
      // save the original result
      char result = Adaptation.getResult(r[r.length - 1]);

      // calculate NN result and test it
      if (Adaptation.getResult(nn.process(r)[0]) == result)
        ++success;
    }

    // error in %
    return (set.length - success) * 100 / set.length;
  }

  private static void download() throws IOException
  {
    PrintWriter pw = new PrintWriter(FILE);

    try
    {
      for (int i = 0; i < 60000 / CHUNK_SIZE; ++i)
      {
        int start = CHUNK_SIZE * i;

        System.out.println("fetching chunk #" + i);

        URL url = new URL(URL + "/bid/data/get?s=" + start + "&c=" + CHUNK_SIZE);
        BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()));
        String line = br.readLine();
        ResponseBidDataGet rbdg = Game.gson.fromJson(line, ResponseBidDataGet.class);

        System.out.println("  got " + rbdg.records.size() + " records");

        if (rbdg.records.size() == 0)
          break;

        pw.println(line);
      }
    }
    finally
    {
      pw.close();
    }
  }

  private static void upload(String fn) throws IOException
  {
    FileInputStream fis = new FileInputStream(fn);
    byte[] data = new byte[fis.available()];

    System.out.println("uploading NN data from file [" + fn + "]... ");

    if (fis.read(data) == data.length)
    {
      RequestConfigBidNNUpdate r = new RequestConfigBidNNUpdate();

      r.data = Convert.b2s(data);

      System.out.println("hex encoded NN size " + r.data.length());

      post("/config/bidnn/update", r);
    }

    fis.close();
  }

  private static void check(byte[] data) throws IOException
  {
    String s = Convert.b2s(data);
    byte[] d = Convert.s2b(s);

    if (d.length != data.length)
      throw new IOException("binary lengths differ");

    for (int i = 0; i < data.length; ++i)
      if (d[i] != data[i])
        throw new IOException("binary data differs at offset " + i + ", " + d[i] + " <-> " + data[i]);
  }

  private static void post(String path, Object object) throws IOException
  {
    byte[] data = new Gson().toJson(object).getBytes("UTF-8");
    HttpURLConnection huc = (HttpURLConnection)new URL(URL + path).openConnection();

    huc.setDoOutput(true);
    huc.setDoInput(true);
    huc.setRequestMethod("POST");
    huc.setUseCaches(false);

    System.out.println("posting " + data.length + " bytes to [" + path + "]...");

    OutputStream os = huc.getOutputStream();

    os.write(data);
    os.close();

    System.out.println("awaiting response...");

    BufferedReader br = new BufferedReader(new InputStreamReader(huc.getInputStream()));

    for (String s = br.readLine(); s != null; s = br.readLine())
      System.out.println(s);
  }

/*
  private static String time(int t)
  {
    if (t < 1000)
      return t + "ms";

    int s = t / 1000;

    if (s < 60)
      return s + "s";

    int m = s / 60;

    if (m < 60)
  }
*/

  private static final String FILE = "bids.txt";
  private static final int CHUNK_SIZE = 500;
  private static final String URL = "http://11.ferpcenter.appspot.com/";

  private static String getNNFileName(String type, NN nn)
  {
    int hlc = nn.getHiddenLayersCount();
    String name = type + ".";

    for (int i = 0; i < hlc; ++i)
      name += nn.getLayerSize(i) + ".";

    return name + "nn";
  }

  // returns serialized size
  private static long serialize(NN nn, String filename) throws IOException
  {
    System.out.println("serializing NN to [" + filename + "]...");

    ObjectOutputStream os = new ObjectOutputStream(new DeflaterOutputStream(new FileOutputStream(filename), new Deflater()));

    os.writeObject(nn);
    os.close();

    return new File(filename).length();
  }

/*
  private static String serialize(NN nn) throws IOException
  {
    String fn = getFileName(nn);
    System.out.print("serializing NN (" + fn + ")...");

    String json = nn.serialize();
    Deflater deflater = new Deflater();
    FileOutputStream fos = new FileOutputStream(fn);
    DeflaterOutputStream dos = new DeflaterOutputStream(fos, deflater);
    PrintWriter pw = new PrintWriter(dos);

    pw.write(json);
    pw.flush();
    pw.close();

    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(baos);

    oos.writeObject(nn);
    oos.close();

    System.out.println(" size=" + json.length() + ", deflated=" + new File(fn).length() + ", serialized=" + baos.size());

    return json;
  }
*/

  private static NN load(String fn) throws IOException, ClassNotFoundException
  {
    System.out.println("loading NN from file [" + fn + "]... ");

    ObjectInputStream is = new ObjectInputStream(new InflaterInputStream(new FileInputStream(fn), new Inflater()));

    try
    {
      return (NN)is.readObject();
    }
    finally
    {
      is.close();
    }
  }
}
